kotlinx.serialization+sealed classでjsonのdeserializeをする
かなり助かったのですが、importやusageが若干不足しており少し苦労したので、こうした形で書き残しておきます
こういうの書ききったらzennとかに転載したほうがいいんだろうなあ
MIT License Copyright (c) 2020 SlashNephy
JSONサンプル
code:json
{"type":"serverTime","data":{"currentMs":"2021-08-23T00:15:42.534+09:00"}}
code:json
{"type":"seat","data":{"keepIntervalSec":30}}
code:json
{"type":"room","data":{"name":"アリーナ","messageServer":{"uri":"wss://msgd.live2.nicovideo.jp/websocket","type":"niwavided"},"threadId":"M.mNT6fhbAft6ZwWQlbeJvZg","yourPostKey":"","isFirst":true,"waybackkey":"waybackkey","vposBaseTime":"2021-08-22T04:00:00+09:00"}}
code:json
{"type":"ping"}
まず、kotlinx.serialization.jsonでjsonをdeserializeをするには以下のようにします
code:kt
import kotlinx.serialization.json.Json
@Serializable
data class NicoliveWebSocketSystemJson(
val type: String,
val data: Data
) {
@Serializable
data class Data(
// seat
val keepIntervalSec: Long
)
}
Json {
ignoreUnknownKeys = true // 未知のキーを許容する
}.decodeFromString<NicoliveWebSocketSystemJson>(it)
しかし上記jsonのように、typeによってdataの構造が異なる場合、1つのデータクラスでデシリアライズしようとするとキーの過不足が発生します
このようなjsonをうまいことでシリアライズするには、大まかに分けて2つの方法があります
ほかにもあるかも…
1つめはdataをkotlinx.serialization.json.JsonElementとし、typeで分岐した先でdecodeFromJsonElementでdataのみ分けたデータクラスを使用してデシリアライズする方法です
code:kt
import kotlinx.serialization.json.JsonElement
@Serializable
data class NicoliveWebSocketSystemJson(
val type: String,
val data: JsonElement
)
@Serializable
data class NicoliveWebSocketSystemJsonSeat(
val keepIntervalSec: Long
)
val message = Json {
ignoreUnknownKeys = true
}.decodeFromString<NicoliveWebSocketSystemJson>(it)
when (message.type) {
"seat" -> {
val messageData = Json { ignoreUnknownKeys = true }.decodeFromJsonElement<NicoliveWebSocketSystemJsonSeat>(message.data)
println(messageData.keepIntervalSec) // NicoliveWebSocketSystemJsonSeatとして使える
}
}
2つ目はタイトルにもある通り、sealed classとKSerializerベースのデシリアライザを組み合わせてデシリアライザする方法です
まずsealed class
code:Models.kt
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable(with = NicoliveWebSocketSystemJsonDeserializer::class)
sealed class NicoliveWebSocketSystemJson {
abstract val type: String
@Serializable
data class Default(
override val type: String,
) : NicoliveWebSocketSystemJson()
@Serializable
data class Seat(
override val type: String,
val data: Data
) : NicoliveWebSocketSystemJson() {
@Serializable
data class Data(
val keepIntervalSec: Long
)
}
@Serializable
data class Room(
override val type: String,
val data: Data
) : NicoliveWebSocketSystemJson() {
@Serializable
data class Data(
val messageServer: MessageServer,
val threadId: JsonElement
) {
@Serializable
data class MessageServer(val uri: String)
}
}
@Serializable
data class Statistics(
override val type: String,
val data: Data
) : NicoliveWebSocketSystemJson() {
@Serializable
data class Data(
val comments: Int
)
}
}
Serializable withで下記デシリアライズと関連付けます
code:NicoliveWebSocketSystemJsonDeserializer.kt
import kotlinx.serialization.Serializer
import kotlinx.serialization.KSerializer
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.JsonDecoder
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonPrimitive
@Serializer(forClass = NicoliveWebSocketSystemJson::class)
object NicoliveWebSocketSystemJsonDeserializer : KSerializer<NicoliveWebSocketSystemJson> {
override fun deserialize(decoder: Decoder): NicoliveWebSocketSystemJson {
require(decoder is JsonDecoder)
val element = decoder.decodeJsonElement()
require(element is JsonObject)
val serializer = when (element"type"?.jsonPrimitive?.content) { "seat" -> NicoliveWebSocketSystemJson.Seat.serializer()
"room" -> NicoliveWebSocketSystemJson.Room.serializer()
"statistics" -> NicoliveWebSocketSystemJson.Statistics.serializer()
else -> NicoliveWebSocketSystemJson.Default.serializer()
}
return decoder.json.decodeFromJsonElement(serializer, element)
}
override fun serialize(encoder: Encoder, value: NicoliveWebSocketSystemJson) {
throw UnsupportedOperationException("Serialization of NicoliveWebSocketSystemJson is unsupported")
}
}
forClassでSealed Classとつなげます
ただ、現時点では@Serializerは実験的アノテーションなので、ビルドオプションに実験用のアーギュメントが必要です
どうにかして"kotlinx.serialization.ExperimentalSerializationApi"を含めてください
デコードはこのようにします
code:decode.kt
val result = Json {
ignoreUnknownKeys = true // 未知のキーを許容する
}.decodeFromString<NicoliveWebSocketSystemJson>(it)
when (result.type) {
"seat" -> {
require(result is NicoliveWebSocketSystemJson.Seat)
println(result.keepIntervalSec) // NicoliveWebSocketSystemJson.Seatとして使える
}
}